package br.indt.framework.network.http;

import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Hashtable;

import javax.microedition.io.Connector;
import javax.microedition.io.HttpConnection;
import javax.microedition.io.HttpsConnection;

import br.indt.framework.log.MicrologWrapper;
import br.indt.framework.threadpool.ThreadPoolTask;

public class HTTPRequest implements ThreadPoolTask {
	
	private final int READ_BUFFER_SIZE = 4000;
	private final MicrologWrapper log = MicrologWrapper.getLogger(HTTPRequest.class);
	
	private int id = 0;
	private String server = null;	
	private String path = null;
	private byte[] data = null;
	private String contentType = null;
	private String method = null;
	private HTTPRequestListener listener = null;
	private boolean putOrDeleteOperation = false;
	private boolean acceptEncoding = false;
	
	private HttpConnection connection = null;
	private Thread requestThread = null;
	private DataInputStream dis = null;
	private DataOutputStream dos = null;
	private Hashtable queryParameters = null;
	private Hashtable headers = null;	
		
	private static int httpRequestId = 1;
	
	public HTTPRequest(String server, String path, HTTPRequestListener listener) {
		
		if (server == null) {
			throw new IllegalArgumentException("server parameter cannot be null");
		}

		if (path == null) {
			throw new IllegalArgumentException("path parameter cannot be null");
		}		
		
		if (listener == null) {
			throw new IllegalArgumentException("listener parameter cannot be null");
		}
		
		this.server = server;
		this.path = path;
		this.listener = listener;
		
		headers = new Hashtable();
		queryParameters = new Hashtable();

	}
	
	public void addHeader(String key, String value) {
		headers.put(key, value);
	}

	public void addQueryParameter(String key, String value) {
		queryParameters.put(key, value);
	}
	
	public synchronized int getId() {
		if (id == 0) {
			id = httpRequestId++;
		}
		return id;
	}

	public byte[] getData() {
		return data;
	}

	public void setData(byte[] data) {
		this.data = data;
	}

	public String getContentType() {
		return contentType;
	}

	public void setContentType(String contentType) {
		this.contentType = contentType;
	}

	public String getMethod() {
		return method;
	}

	public void setMethod(String method) {
		this.method = method;
	}

	public boolean isPutOrDeleteOperation() {
		return putOrDeleteOperation;
	}

	public void setPutOrDeleteOperation(boolean putOrDeleteOperation) {
		this.putOrDeleteOperation = putOrDeleteOperation;
	}

	public boolean isAcceptEncoding() {
		return acceptEncoding;
	}

	public void setAcceptEncoding(boolean acceptEncoding) {
		this.acceptEncoding = acceptEncoding;
	}

	public String getServer() {
		return server;
	}

	public String getPath() {
		return path;
	}

	public HTTPRequestListener getListener() {
		return listener;
	}	

	public Hashtable getQueryParameters() {
		return queryParameters;
	}

	public Hashtable getHeaders() {
		return headers;
	}

	public void execute() throws InterruptedException {
		log.debug("HTTPRequest::execute - start - " + id);
		
		requestThread = new RequestThread();
		requestThread.start();
		requestThread.join();
		
		log.debug("HTTPRequest::execute - finish - " + id);
	}

	protected String getURL() {
		StringBuffer sb = new StringBuffer();
		sb.append(server);

		if(!server.endsWith("/") && !path.startsWith("/")) {
			sb.append("/");
		}

		sb.append(path);

		if(queryParameters.size() > 0) {
			sb.append("?");
			Enumeration enumeration = queryParameters.keys();
			while(enumeration.hasMoreElements()) {
				String key = (String) enumeration.nextElement();
				String value = (String) queryParameters.get(key);
				sb.append(key).append("=").append(value).append((enumeration.hasMoreElements()) ? "&" : "");
			}
		}

		log.debug("Using " + sb.toString() + " for http request - " + id);
		
		return sb.toString();
	}

	protected void setHeaders(HttpConnection connection) throws IOException   {
		if(headers == null || headers.size() == 0) {
			log.debug("no headers for request - " + id);
			return;
		}

		Enumeration enumeration = headers.keys();
		while(enumeration.hasMoreElements()) {
			String key = (String) enumeration.nextElement();
			String value = (String) headers.get(key);
			int i = key.indexOf("#");
			if(i != -1) {
				key = key.substring(0, i);
			}
			connection.setRequestProperty(key, value);
		}

		if (acceptEncoding){
			connection.setRequestProperty("Accept-Encoding", "gzip");
		}
	}

	protected void notifyErrorToListener(int errorCode) {
		if(listener != null) {
			listener.httpRequestError(this, errorCode);
		}
	}
	
	protected void notifyIncomingData(byte[] data, int index, int len, boolean firstResponse, boolean completed) {
		if (listener != null) {
			listener.httpRequestIncomingData(this, data, index, len, firstResponse, completed);
		}
	}

	protected void handleHTTPResponseCode(int code) {
		switch (code) {
		case HttpConnection.HTTP_UNAUTHORIZED:
		case HttpConnection.HTTP_FORBIDDEN:
			notifyErrorToListener(ErrorCodes.NOT_AUTHORIZED);
			break;
		case HttpConnection.HTTP_CONFLICT:
			notifyErrorToListener(ErrorCodes.CONFLICT);
			break;
		case HttpConnection.HTTP_INTERNAL_ERROR:
		case HttpConnection.HTTP_NOT_IMPLEMENTED:
		case HttpConnection.HTTP_BAD_GATEWAY:
		case HttpConnection.HTTP_UNAVAILABLE:
		case HttpConnection.HTTP_GATEWAY_TIMEOUT:
		case HttpConnection.HTTP_VERSION:
			notifyErrorToListener(ErrorCodes.SERVER_ERROR);
			break;
		default:
			notifyErrorToListener(ErrorCodes.CONNECTION_ERROR);
			break;
		}
	}

	protected boolean readDataFromConnection(HttpConnection connection) throws Exception {
		try {
			
			if (connection.getResponseCode() == HttpConnection.HTTP_NO_CONTENT) {				
				log.debug("http request " + id + " no content");
				notifyIncomingData(null, 0, 0, false, true);
			} else {
				int eof = 0;
				byte buffer[] = new byte[READ_BUFFER_SIZE];
				boolean firstResponse = true;
				dis = connection.openDataInputStream();
				
				while(eof != -1) {
					eof = dis.read(buffer);
					if(eof != -1 && eof != 0) {
						notifyIncomingData(buffer, 0, eof, firstResponse, false);
						firstResponse = false;
					} else {
						log.debug("http request " + id + " notify end of incoming data");
						notifyIncomingData(null, 0, 0, false, true);						
					}
				}
			}

		} catch(Exception e) {
			log.error("readDataFromConnection - " + e.getMessage() );
			return false;

		} finally {
			try{if(dis!=null)dis.close();dis=null;}catch(Exception e){}
		}
		return true;
	}

	//#ifdef LOGS
	protected boolean readErrorFromConnection(HttpConnection connection) throws Exception {
		try {
			
			if (connection.getResponseCode() != HttpConnection.HTTP_NO_CONTENT) {
				
				ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
				byte buffer[] = new byte[READ_BUFFER_SIZE];
				int eof = 0;
				
				dis = connection.openDataInputStream();
								
				while(eof != -1) {
					eof = dis.read(buffer);
					if(eof != -1 && eof != 0) {
						errorStream.write(buffer, 0, eof);
					}
				}
				
				log.debug(new String(errorStream.toByteArray(), "UTF-8"));
			}

		} catch(Exception e) {
			log.error("readErrorFromConnection - " + e.getMessage() );
			return false;

		} finally {
			try{if(dis!=null)dis.close();dis=null;}catch(Exception e){}
		}
		return true;
	}
	//#endif
	
	protected boolean sendData() {
		if(data != null) {
			this.method = HttpConnection.POST;
			sendData(this.method);
		}
		return true;
	}

	protected boolean sendData(String method) {
		if(data != null) {
			try {
				connection.setRequestMethod(method);
				connection.setRequestProperty("Content-Type", contentType);
				log.debug("method:"+method +" contentType:"+contentType + "data"+data.length);
				dos = connection.openDataOutputStream();
				dos.write(data);
				dos.flush();

			} catch(Exception e) {
				log.error("in sendData" + e);
				return false;

			} finally {
				try{if(dos!=null)dos.close();dos=null;}catch(Exception e){}
			}
		}
		return true;
	}

	public void cancel() {		
		log.debug("HTTPRequest::cancel - start - " + id);
		
		try{ requestThread.interrupt(); } catch (Exception e) { }
		
		new Thread(new Runnable() {
			public void run() {
				try{ HTTPRequest.this.dos.close(); } catch (Exception e) { }
				try{ HTTPRequest.this.connection.close(); } catch (Exception e) { }
				try{ HTTPRequest.this.dis.close(); } catch (Exception e) { }				
			}
		}).start();
		
		log.debug("HTTPRequest::cancel - finish - " + id);
	}
	
	/**
	 * Internal class RequestThread executes the HTTP request in a separated thread
	 * @author sifilho
	 *
	 */
	
	private class RequestThread extends Thread {
		
		public void run() {
			String url = getURL();

			try	{
				
				if (url.startsWith("https://"))	{
					connection = (HttpsConnection) Connector.open(url);
				} else {
					connection = (HttpConnection) Connector.open(url);
				}
				
				setHeaders(connection);

				if (!sendData()) {
					notifyErrorToListener(ErrorCodes.UNABLE_TO_SEND_POST_DATA);
					return;

				} else {
					if (putOrDeleteOperation && method != null && method.equalsIgnoreCase(HttpConnection.POST)) {
						connection.setRequestMethod(HttpConnection.POST);
					}
				}

				int resCode = connection.getResponseCode();
				
				log.debug("HTTPRequest::execute - " + id + " resCode - " + resCode + " url  " + url);
				
				if (resCode >= HttpConnection.HTTP_OK && resCode <= HttpConnection.HTTP_PARTIAL) {
					
					if (!readDataFromConnection(connection)) {
						notifyErrorToListener(ErrorCodes.UNKNOWN_ERROR);
					}

				} else {
					//#ifdef LOGS
					readErrorFromConnection(connection);
					//#endif
					handleHTTPResponseCode(resCode);
				}

			} catch (Throwable e) {
				log.error("Exception in HTTPRequest::execute - " + id + " exception - " + e + " url  " + url);
				handleHTTPResponseCode(ErrorCodes.CONNECTION_ERROR);

			} finally {
				try {
					if (connection != null) connection.close(); connection = null;
				} catch (Exception e) {
				}
			}
		}
	}
}